// filepath: cmd/fleetctl/generate_gitops_test.go
package fleetctl

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/fleetdm/fleet/v4/pkg/optjson"
	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/fleetdm/fleet/v4/server/ptr"
	"github.com/ghodss/yaml"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/urfave/cli/v2"
)

type MockClient struct {
	IsFree           bool
	TeamNameOverride string
	WithoutMDM       bool
}

func (c *MockClient) GetAppConfig() (*fleet.EnrichedAppConfig, error) {
	b, err := os.ReadFile("./testdata/generateGitops/appConfig.json")
	if err != nil {
		return nil, err
	}
	var appConfig fleet.EnrichedAppConfig
	if err := json.Unmarshal(b, &appConfig); err != nil {
		return nil, err
	}
	if c.IsFree == true {
		appConfig.License.Tier = fleet.TierFree
	}

	if c.WithoutMDM {
		appConfig.MDM.EnabledAndConfigured = false
		appConfig.MDM.AndroidEnabledAndConfigured = false
		appConfig.MDM.WindowsEnabledAndConfigured = false
	}

	return &appConfig, nil
}

func (MockClient) GetEnrollSecretSpec() (*fleet.EnrollSecretSpec, error) {
	spec := &fleet.EnrollSecretSpec{
		Secrets: []*fleet.EnrollSecret{
			{
				Secret: "some-secret-number-one",
			},
			{
				Secret: "some-secret-number-two",
			},
		},
	}
	return spec, nil
}

func (c *MockClient) ListTeams(query string) ([]fleet.Team, error) {
	var config fleet.TeamConfig
	b, err := os.ReadFile("./testdata/generateGitops/teamConfig.json")
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal(b, &config); err != nil {
		return nil, err
	}
	teams := []fleet.Team{
		{
			ID:     1,
			Name:   "Team A 👍",
			Config: config,
			Secrets: []*fleet.EnrollSecret{
				{
					Secret: "some-team-secret",
				},
			},
		},
	}
	if c.TeamNameOverride != "" {
		teams[0].Name = c.TeamNameOverride
	}
	return teams, nil
}

func (MockClient) ListScripts(query string) ([]*fleet.Script, error) {
	switch query {
	case "team_id=1":
		return []*fleet.Script{{
			ID:              2,
			TeamID:          ptr.Uint(1),
			Name:            "Script B.ps1",
			ScriptContentID: 2,
		}}, nil
	case "team_id=0":
		return []*fleet.Script{{
			ID:              3,
			TeamID:          ptr.Uint(0),
			Name:            "Script Z.ps1",
			ScriptContentID: 3,
		}}, nil
	case "team_id=2", "team_id=3", "team_id=4", "team_id=5":
		return nil, nil
	case "team_id=6":
		return nil, nil
	default:
		return nil, fmt.Errorf("unexpected query: %s", query)
	}
}

func (c MockClient) ListConfigurationProfiles(teamID *uint) ([]*fleet.MDMConfigProfilePayload, error) {
	if c.WithoutMDM {
		return nil, errors.New("should not have pulled configuration profiles endpoint")
	}

	if teamID == nil {
		return []*fleet.MDMConfigProfilePayload{
			{
				ProfileUUID: "global-macos-mobileconfig-profile-uuid",
				Name:        "Global MacOS MobileConfig Profile",
				Platform:    "darwin",
				Identifier:  "com.example.global-macos-mobileconfig-profile",
				LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{
					LabelName: "Label A",
				}, {
					LabelName: "Label B",
				}},
			},
			{
				ProfileUUID: "global-macos-json-profile-uuid",
				Name:        "Global MacOS JSON Profile",
				Platform:    "darwin",
				Identifier:  "com.example.global-macos-json-profile",
				LabelsExcludeAny: []fleet.ConfigurationProfileLabel{{
					LabelName: "Label C",
				}},
			},
			{
				ProfileUUID: "global-windows-profile-uuid",
				Name:        "Global Windows Profile",
				Platform:    "windows",
				LabelsIncludeAny: []fleet.ConfigurationProfileLabel{{
					LabelName: "Label D",
				}},
			},
			{
				ProfileUUID: "global-android-profile-uuid",
				Name:        "Global Android Profile",
				Platform:    "android",
			},
		}, nil
	}
	if *teamID == 1 {
		return []*fleet.MDMConfigProfilePayload{
			{
				ProfileUUID: "test-mobileconfig-profile-uuid",
				Name:        "Team MacOS MobileConfig Profile",
				Platform:    "darwin",
				Identifier:  "com.example.team-macos-mobileconfig-profile",
			},
		}, nil
	}
	if *teamID == 0 || *teamID == 2 || *teamID == 3 || *teamID == 4 || *teamID == 5 || *teamID == 6 {
		return nil, nil
	}
	return nil, fmt.Errorf("unexpected team ID: %v", *teamID)
}

func (MockClient) GetScriptContents(scriptID uint) ([]byte, error) {
	if scriptID == 2 {
		return []byte("pop goes the weasel!"), nil
	}
	if scriptID == 3 {
		return []byte("#!/usr/bin/env pwsh\necho \"Hello from Script B!\""), nil
	}
	return nil, errors.New("script not found")
}

func (MockClient) GetProfileContents(profileID string) ([]byte, error) {
	switch profileID {
	case "global-macos-mobileconfig-profile-uuid":
		return []byte("<xml>global macos mobileconfig profile</xml>"), nil
	case "global-macos-json-profile-uuid":
		return []byte(`{"profile": "global macos json profile"}`), nil
	case "global-windows-profile-uuid":
		return []byte("<xml>global windows profile</xml>"), nil
	case "global-android-profile-uuid":
		return []byte(`{"name": "Global Android Profile", "cameraDisabled": true}`), nil
	case "test-mobileconfig-profile-uuid":
		return []byte("<xml>test mobileconfig profile</xml>"), nil
	}
	return nil, errors.New("profile not found")
}

func (MockClient) GetTeam(teamID uint) (*fleet.Team, error) {
	if teamID == 0 {
		// Return "No Team" configuration with webhook settings
		return &fleet.Team{
			ID:   0,
			Name: "No team",
			Config: fleet.TeamConfig{
				WebhookSettings: fleet.TeamWebhookSettings{
					FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{
						Enable:         true,
						DestinationURL: "https://example.com/no-team-webhook",
						PolicyIDs:      []uint{1, 2, 3},
						HostBatchSize:  100,
					},
				},
			},
		}, nil
	}
	if teamID == 1 {
		b, err := os.ReadFile("./testdata/generateGitops/teamConfig.json")
		if err != nil {
			return nil, err
		}
		var config fleet.TeamConfig
		if err := json.Unmarshal(b, &config); err != nil {
			return nil, err
		}
		return &fleet.Team{
			ID:     1,
			Name:   "Test Team",
			Config: config,
			Secrets: []*fleet.EnrollSecret{
				{
					Secret: "some-team-secret",
				},
			},
		}, nil
	}

	return nil, errors.New("team not found")
}

func (MockClient) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResult, error) {
	switch query {
	case "available_for_install=1&team_id=1":
		return []fleet.SoftwareTitleListResult{
			{
				ID:         1,
				Name:       "My Software Package",
				HashSHA256: ptr.String("software-package-hash"),
				SoftwarePackage: &fleet.SoftwarePackageOrApp{
					Name:     "my-software.pkg",
					Platform: "darwin",
					Version:  "13.37",
				},
			},
			{
				ID:   2,
				Name: "My App Store App",
				AppStoreApp: &fleet.SoftwarePackageOrApp{
					AppStoreID: "com.example.team-software",
					Platform:   string(fleet.MacOSPlatform),
				},
				HashSHA256: ptr.String("app-store-app-hash"),
			},
		}, nil
	case "available_for_install=1&team_id=0":
		return []fleet.SoftwareTitleListResult{}, nil
	default:
		return nil, fmt.Errorf("unexpected query: %s", query)
	}
}

func (MockClient) GetPolicies(teamID *uint) ([]*fleet.Policy, error) {
	if teamID == nil {
		return []*fleet.Policy{
			{
				PolicyData: fleet.PolicyData{
					ID:          1,
					Name:        "Global Policy",
					Query:       "SELECT * FROM global_policy WHERE id = 1",
					Resolution:  ptr.String("Do a global thing"),
					Description: "This is a global policy",
					Platform:    "darwin",
					LabelsIncludeAny: []fleet.LabelIdent{{
						LabelName: "Label A",
					}, {
						LabelName: "Label B",
					}},
					ConditionalAccessEnabled: true,
				},
				InstallSoftware: &fleet.PolicySoftwareTitle{
					SoftwareTitleID: 1,
				},
			},
		}, nil
	}
	return []*fleet.Policy{
		{
			PolicyData: fleet.PolicyData{
				ID:                       1,
				Name:                     "Team Policy",
				Query:                    "SELECT * FROM team_policy WHERE id = 1",
				Resolution:               ptr.String("Do a team thing"),
				Description:              "This is a team policy",
				Platform:                 "linux,windows",
				ConditionalAccessEnabled: true,
			},
			RunScript: &fleet.PolicyScript{
				ID: 1,
			},
		},
	}, nil
}

func (MockClient) GetQueries(teamID *uint, name *string) ([]fleet.Query, error) {
	if teamID == nil {
		return []fleet.Query{
			{
				ID:                 1,
				Name:               "Global Query",
				Query:              "SELECT * FROM global_query WHERE id = 1",
				Description:        "This is a global query",
				Platform:           "darwin",
				Interval:           3600,
				ObserverCanRun:     true,
				AutomationsEnabled: true,
				LabelsIncludeAny: []fleet.LabelIdent{{
					LabelName: "Label A",
				}, {
					LabelName: "Label B",
				}},
				MinOsqueryVersion: "1.2.3",
				Logging:           "stdout",
			},
		}, nil
	}
	return []fleet.Query{
		{
			ID:                 1,
			Name:               "Team Query",
			Query:              "SELECT * FROM team_query WHERE id = 1",
			Description:        "This is a team query",
			Platform:           "linux,windows",
			Interval:           1800,
			ObserverCanRun:     false,
			AutomationsEnabled: true,
			MinOsqueryVersion:  "4.5.6",
			Logging:            "stderr",
		},
	}, nil
}

//nolint:gocritic // ignore captLocal
func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTitle, error) {
	switch ID {
	case 1:
		if *teamID != 1 {
			return nil, errors.New("team ID mismatch")
		}
		return &fleet.SoftwareTitle{
			ID: 1,
			SoftwarePackage: &fleet.SoftwareInstaller{
				LabelsIncludeAny: []fleet.SoftwareScopeLabel{{
					LabelName: "Label A",
				}, {
					LabelName: "Label B",
				}},
				PreInstallQuery:   "SELECT * FROM pre_install_query",
				InstallScript:     "foo",
				PostInstallScript: "bar",
				UninstallScript:   "baz",
				SelfService:       true,
				Platform:          "darwin",
				URL:               "https://example.com/download/my-software.pkg",
				Categories:        []string{"Browsers"},
			},
			IconUrl: ptr.String("/api/icon1.png"),
		}, nil
	case 2:
		if *teamID != 1 {
			return nil, errors.New("team ID mismatch")
		}
		return &fleet.SoftwareTitle{
			ID: 2,
			AppStoreApp: &fleet.VPPAppStoreApp{
				VPPAppID: fleet.VPPAppID{Platform: fleet.MacOSPlatform},
				LabelsExcludeAny: []fleet.SoftwareScopeLabel{{
					LabelName: "Label C",
				}, {
					LabelName: "Label D",
				}},
				Categories: []string{"Productivity", "Utilities"},

				SelfService: true,
			},
			IconUrl: ptr.String("/api/icon2.png"),
		}, nil
	default:
		return nil, errors.New("software title not found")
	}
}

func (MockClient) GetSoftwareTitleIcon(titleID uint, teamID uint) ([]byte, error) {
	return []byte(fmt.Sprintf("icon for title %d on team %d", titleID, teamID)), nil
}

func (MockClient) GetLabels() ([]*fleet.LabelSpec, error) {
	return []*fleet.LabelSpec{{
		Name:                "Label A",
		Platform:            "linux,macos",
		Description:         "Label A description",
		LabelMembershipType: fleet.LabelMembershipTypeDynamic,
		Query:               "SELECT * FROM osquery_info",
	}, {
		Name:                "Label B",
		Description:         "Label B description",
		LabelMembershipType: fleet.LabelMembershipTypeManual,
		Hosts:               []string{"1", "2"},
	}, {
		Name:                "Label C",
		Description:         "Label C description",
		LabelMembershipType: fleet.LabelMembershipTypeHostVitals,
		HostVitalsCriteria:  ptr.RawMessage(json.RawMessage(`{"vital": "end_user_idp_group", "value": "some-group"}`)),
	}}, nil
}

func (MockClient) Me() (*fleet.User, error) {
	return &fleet.User{
		ID:         1,
		Name:       "Test User",
		Email:      "test@example.com",
		GlobalRole: ptr.String("admin"),
	}, nil
}

func (MockClient) GetEULAMetadata() (*fleet.MDMEULA, error) {
	return &fleet.MDMEULA{
		Name:  "test.pdf",
		Token: "test-eula-token",
	}, nil
}

func (MockClient) GetEULAContent(token string) ([]byte, error) {
	return []byte("This is the EULA content."), nil
}

func (MockClient) GetSetupExperienceSoftware(platform string, teamID uint) ([]fleet.SoftwareTitleListResult, error) {
	if teamID == 1 {
		return []fleet.SoftwareTitleListResult{
			{
				ID:         1,
				Name:       "My Software Package",
				HashSHA256: ptr.String("software-package-hash"),
				SoftwarePackage: &fleet.SoftwarePackageOrApp{
					InstallDuringSetup: ptr.Bool(true),
					Name:               "my-software.pkg",
					Platform:           "darwin",
					Version:            "13.37",
				},
			},
		}, nil
	}
	if teamID == 2 {
		return []fleet.SoftwareTitleListResult{
			{
				ID:         1,
				Name:       "My Software Package",
				HashSHA256: ptr.String("software-package-hash"),
				SoftwarePackage: &fleet.SoftwarePackageOrApp{
					InstallDuringSetup: ptr.Bool(false),
					Name:               "my-software.pkg",
					Platform:           "darwin",
					Version:            "13.37",
				},
			},
			{
				ID:         2,
				Name:       "My Other Software Package",
				HashSHA256: ptr.String("software-package-hash"),
			},
		}, nil
	}
	if teamID == 0 || teamID == 3 || teamID == 4 || teamID == 5 {
		return nil, nil
	}
	return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}

func (MockClient) GetBootstrapPackageMetadata(teamID uint, forUpdate bool) (*fleet.MDMAppleBootstrapPackage, error) {
	if teamID == 3 {
		return &fleet.MDMAppleBootstrapPackage{
			Name: "Bootstrap Package for Team 1",
		}, nil
	}
	if teamID == 0 || teamID == 1 || teamID == 2 || teamID == 4 || teamID == 5 {
		return nil, nil
	}
	return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}

func (MockClient) GetSetupExperienceScript(teamID uint) (*fleet.Script, error) {
	if teamID == 4 {
		return &fleet.Script{
			Name: "Setup Experience Script for Team 1",
		}, nil
	}
	if teamID == 0 || teamID == 1 || teamID == 2 || teamID == 3 || teamID == 5 {
		return nil, nil
	}
	return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}

func (MockClient) GetAppleMDMEnrollmentProfile(teamID uint) (*fleet.MDMAppleSetupAssistant, error) {
	if teamID == 5 {
		return &fleet.MDMAppleSetupAssistant{
			Name: "Apple MDM Enrollment Profile for Team 1",
		}, nil
	}
	if teamID == 0 || teamID == 1 || teamID == 2 || teamID == 3 || teamID == 4 {
		return nil, nil
	}
	return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}

func (MockClient) GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
	res := fleet.GroupedCertificateAuthorities{
		DigiCert: []fleet.DigiCertCA{
			{
				Name:                  "some-digicert-name",
				URL:                   "https://some-digicert-url.com",
				APIToken:              maskSecret("some-digicert-api-token", includeSecrets),
				ProfileID:             "some-digicert-profile-id",
				CertificateCommonName: "some-digicert-certificate-common-name",
				CertificateUserPrincipalNames: []string{
					"some-digicert-certificate-user-principal-name",
					"some-other-digicert-certificate-user-principal-name",
				},
				CertificateSeatID: "some-digicert-certificate-seat-id",
			},
		},
		NDESSCEP: &fleet.NDESSCEPProxyCA{
			URL:      "https://some-ndes-scep-proxy-url.com",
			AdminURL: "https://some-ndes-admin-url.com",
			Username: "some-ndes-username",
			Password: maskSecret("some-ndes-password", includeSecrets),
		},
		CustomScepProxy: []fleet.CustomSCEPProxyCA{
			{
				Name:      "some-custom-scep-proxy-name",
				URL:       "https://some-custom-scep-proxy-url.com",
				Challenge: maskSecret("some-custom-scep-proxy-challenge", includeSecrets),
			},
		},
		Hydrant: []fleet.HydrantCA{
			{
				Name:         "some-hydrant-name",
				URL:          "https://some-hydrant-url.com",
				ClientID:     "some-hydrant-client-id",
				ClientSecret: maskSecret("some-hydrant-client-secret", includeSecrets),
			},
		},
		EST: []fleet.ESTProxyCA{
			{
				Name:     "some-est-name",
				URL:      "https://some-est-url.example.com",
				Username: "some-est-username",
				Password: maskSecret("some-est-password", includeSecrets),
			},
		},
		Smallstep: []fleet.SmallstepSCEPProxyCA{
			{
				Name:         "some-smallstep-name",
				URL:          "https://some-smallstep-url.com",
				ChallengeURL: "https://some-smallstep-challenge-url.com",
				Username:     "some-smallstep-username",
				Password:     maskSecret("some-smallstep-password", includeSecrets),
			},
		},
	}

	return &res, nil
}

func (MockClient) GetCertificateTemplates(teamID string) ([]*fleet.CertificateTemplateResponseSummary, error) {
	var res []*fleet.CertificateTemplateResponseSummary
	if teamID == "1" {
		res = []*fleet.CertificateTemplateResponseSummary{
			{
				ID:                       1,
				CertificateAuthorityName: "DIGIDOO",
				Name:                     "my_certypoo",
			},
		}
	}
	return res, nil
}

func (MockClient) GetCertificateTemplate(certificateID uint, hostUUID *string) (*fleet.CertificateTemplateResponseFull, error) {
	var res *fleet.CertificateTemplateResponseFull
	if certificateID == 1 {
		res = &fleet.CertificateTemplateResponseFull{
			CertificateTemplateResponseSummary: fleet.CertificateTemplateResponseSummary{
				ID:                       1,
				CertificateAuthorityName: "DIGIDOO",
				Name:                     "my_certypoo",
			},
			SubjectName: "CN=OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL",
		}
	}
	return res, nil
}

func maskSecret(value string, shouldShowSecret bool) string {
	if shouldShowSecret {
		return value
	}
	return "********"
}

func compareDirs(t *testing.T, sourceDir, targetDir string) {
	err := filepath.WalkDir(sourceDir, func(srcPath string, d os.DirEntry, walkErr error) error {
		if d.IsDir() {
			return nil
		}

		relPath, err := filepath.Rel(sourceDir, srcPath)
		require.NoError(t, err, "Error getting relative path: %v", err)
		// Patch these files because Go can't zip the module with emojis in the filename.
		// The actual outputted file will have the emoji, but the testdata file can't.
		relPath = strings.Replace(relPath, "team-a-thumbsup", "team-a-👍", 1)

		tgtPath := filepath.Join(targetDir, relPath)

		targetInfo, err := os.Stat(tgtPath)
		require.NoError(t, err, "Error getting target file info: %v", err)
		require.False(t, targetInfo.IsDir(), "Expected file but found directory: %s", tgtPath)

		srcData, err := os.ReadFile(srcPath)
		require.NoError(t, err, "Error reading source file: %v", err)

		tgtData, err := os.ReadFile(tgtPath)
		require.NoError(t, err, "Error reading target file: %v", err)

		require.Equal(t, string(srcData), string(tgtData), "File contents do not match for %s", relPath)

		return nil
	})
	if err != nil {
		t.Fatalf("Error walking source directory: %v", err)
	}
}

func TestGenerateGitops(t *testing.T) {
	fleetClient := &MockClient{}
	action := createGenerateGitopsAction(fleetClient)
	buf := new(bytes.Buffer)
	tempDir := os.TempDir() + "/" + uuid.New().String()
	flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
	flagSet.String("dir", tempDir, "")

	cliContext := cli.NewContext(&cli.App{
		Name:      "test",
		Usage:     "test",
		Writer:    buf,
		ErrWriter: buf,
	}, flagSet, nil)
	err := action(cliContext)
	require.NoError(t, err, buf.String())

	compareDirs(t, "./testdata/generateGitops/test_dir_premium", tempDir)

	t.Cleanup(func() {
		if err := os.RemoveAll(tempDir); err != nil {
			t.Fatalf("failed to remove temp dir: %v", err)
		}
	})
}

func TestGenerateGitopsWithoutMDM(t *testing.T) {
	fleetClient := &MockClient{WithoutMDM: true}
	action := createGenerateGitopsAction(fleetClient)
	buf := new(bytes.Buffer)
	tempDir := os.TempDir() + "/" + uuid.New().String()
	flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
	flagSet.String("dir", tempDir, "")

	cliContext := cli.NewContext(&cli.App{
		Name:      "test",
		Usage:     "test",
		Writer:    buf,
		ErrWriter: buf,
	}, flagSet, nil)
	err := action(cliContext)
	require.NoError(t, err, buf.String()) // just checking for success to verify #33667

	t.Cleanup(func() {
		if err := os.RemoveAll(tempDir); err != nil {
			t.Fatalf("failed to remove temp dir: %v", err)
		}
	})
}

func TestGenerateGitopsFree(t *testing.T) {
	fleetClient := &MockClient{}
	fleetClient.IsFree = true
	action := createGenerateGitopsAction(fleetClient)
	buf := new(bytes.Buffer)
	tempDir := os.TempDir() + "/" + uuid.New().String()
	flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
	flagSet.String("dir", tempDir, "")

	cliContext := cli.NewContext(&cli.App{
		Name:      "test",
		Usage:     "test",
		Writer:    buf,
		ErrWriter: buf,
	}, flagSet, nil)
	err := action(cliContext)
	require.NoError(t, err, buf.String())

	compareDirs(t, "./testdata/generateGitops/test_dir_free", tempDir)

	t.Cleanup(func() {
		if err := os.RemoveAll(tempDir); err != nil {
			t.Fatalf("failed to remove temp dir: %v", err)
		}
	})
}

func TestGenerateOrgSettings(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(&cli.App{}, nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
	}

	// Generate the org settings.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	orgSettingsRaw, err := cmd.generateOrgSettings()
	require.NoError(t, err)
	require.NotNil(t, orgSettingsRaw)
	var orgSettings map[string]interface{}
	b, err := yaml.Marshal(orgSettingsRaw)
	require.NoError(t, err)
	fmt.Println("Org settings raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &orgSettings)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedOrgSettings.yaml")
	require.NoError(t, err)
	var expectedAppConfig map[string]interface{}
	err = yaml.Unmarshal(b, &expectedAppConfig)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedAppConfig, orgSettings)
}

func TestGeneratedOrgSettingsNoSSO(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	appConfig.SSOSettings = nil

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(&cli.App{}, nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
	}

	// Generate the org settings.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	orgSettingsRaw, err := cmd.generateOrgSettings()
	require.NoError(t, err)
	require.NotNil(t, orgSettingsRaw)
	var orgSettings map[string]any
	b, err := yaml.Marshal(orgSettingsRaw)
	require.NoError(t, err)
	err = yaml.Unmarshal(b, &orgSettings)
	require.NoError(t, err)
}

func TestGeneratedOrgSettingsOktaConditionalAccessNotIncluded(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	// Set Okta conditional access fields (these should NOT appear in GitOps output)
	appConfig.ConditionalAccess = &fleet.ConditionalAccessSettings{
		MicrosoftEntraTenantID:             "test-tenant-id",
		MicrosoftEntraConnectionConfigured: true,
		OktaIDPID:                          optjson.SetString("https://okta.example.com/idp"),
		OktaAssertionConsumerServiceURL:    optjson.SetString("https://okta.example.com/acs"),
		OktaAudienceURI:                    optjson.SetString("https://okta.example.com/audience"),
		OktaCertificate:                    optjson.SetString("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"),
	}

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(&cli.App{}, nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
	}

	// Generate the org settings.
	orgSettingsRaw, err := cmd.generateOrgSettings()
	require.NoError(t, err)
	require.NotNil(t, orgSettingsRaw)
	var orgSettings map[string]any
	b, err := yaml.Marshal(orgSettingsRaw)
	require.NoError(t, err)
	err = yaml.Unmarshal(b, &orgSettings)
	require.NoError(t, err)

	// Verify that conditional_access section does not exist in the output
	// (Okta configs are not supported in GitOps)
	_, hasConditionalAccess := orgSettings["conditional_access"]
	assert.False(t, hasConditionalAccess, "conditional_access section should not be present in GitOps output as Okta configs are not supported")

	// Also verify by checking the YAML string directly
	yamlStr := string(b)
	assert.NotContains(t, yamlStr, "okta_idp_id", "Okta IDP ID should not be in GitOps output")
	assert.NotContains(t, yamlStr, "okta_assertion_consumer_service_url", "Okta ACS URL should not be in GitOps output")
	assert.NotContains(t, yamlStr, "okta_audience_uri", "Okta Audience URI should not be in GitOps output")
	assert.NotContains(t, yamlStr, "okta_certificate", "Okta Certificate should not be in GitOps output")
	assert.NotContains(t, yamlStr, "microsoft_entra_tenant_id", "Microsoft Entra Tenant ID should not be in GitOps output")
	assert.NotContains(t, yamlStr, "microsoft_entra_connection_configured", "Microsoft Entra connection status should not be in GitOps output")
}

func TestGenerateOrgSettingsInsecure(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
	flagSet.Bool("insecure", true, "Output sensitive information in plaintext.")
	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(&cli.App{}, flagSet, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
	}

	// Generate the org settings.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	orgSettingsRaw, err := cmd.generateOrgSettings()
	require.NoError(t, err)
	require.NotNil(t, orgSettingsRaw)
	var orgSettings map[string]interface{}
	b, err := yaml.Marshal(orgSettingsRaw)
	require.NoError(t, err)
	fmt.Println("Org settings raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &orgSettings)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedOrgSettings-insecure.yaml")
	require.NoError(t, err)
	var expectedAppConfig map[string]interface{}
	err = yaml.Unmarshal(b, &expectedAppConfig)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedAppConfig, orgSettings)
}

func TestGenerateTeamSettings(t *testing.T) {
	// Get the test team.
	fleetClient := &MockClient{}
	team, err := fleetClient.GetTeam(1)
	require.NoError(t, err)

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(&cli.App{}, nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    nil,
	}

	// Generate the org settings.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	TeamSettingsRaw, err := cmd.generateTeamSettings("team.yml", team)
	require.NoError(t, err)
	require.NotNil(t, TeamSettingsRaw)
	var TeamSettings map[string]interface{}
	b, err := yaml.Marshal(TeamSettingsRaw)
	require.NoError(t, err)
	fmt.Println("Team settings raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &TeamSettings)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSettings.yaml")
	require.NoError(t, err)
	var expectedAppConfig map[string]interface{}
	err = yaml.Unmarshal(b, &expectedAppConfig)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedAppConfig, TeamSettings)
}

func TestGenerateTeamSettingsInsecure(t *testing.T) {
	// Get the test team.
	fleetClient := &MockClient{}
	team, err := fleetClient.GetTeam(1)
	require.NoError(t, err)

	flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
	flagSet.Bool("insecure", true, "Output sensitive information in plaintext.")
	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(&cli.App{}, flagSet, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    nil,
	}

	// Generate the org settings.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	TeamSettingsRaw, err := cmd.generateTeamSettings("team.yml", team)
	require.NoError(t, err)
	require.NotNil(t, TeamSettingsRaw)
	var TeamSettings map[string]interface{}
	b, err := yaml.Marshal(TeamSettingsRaw)
	require.NoError(t, err)
	fmt.Println("Team settings raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &TeamSettings)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSettings-insecure.yaml")
	require.NoError(t, err)
	var expectedAppConfig map[string]interface{}
	err = yaml.Unmarshal(b, &expectedAppConfig)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedAppConfig, TeamSettings)
}

// For the purpose of testing macos_setup generation,
// Team 1 has setup experience software with InstallDuringSetup enabled,
// Team 2 has setup experience software with InstallDuringSetup disabled,
// Team 3 has a bootstrap package,
// Team 4 has a setup experience script,
// Team 5 has an Apple MDM enrollment profile.
func TestGenerateControls(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(cli.NewApp(), nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
		ScriptList:   make(map[uint]string),
	}

	// Generate global controls.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	mdmConfig := fleet.TeamMDM{
		EnableDiskEncryption: appConfig.MDM.EnableDiskEncryption.Value,
		MacOSUpdates:         appConfig.MDM.MacOSUpdates,
		IOSUpdates:           appConfig.MDM.IOSUpdates,
		IPadOSUpdates:        appConfig.MDM.IPadOSUpdates,
		WindowsUpdates:       appConfig.MDM.WindowsUpdates,
		MacOSSetup:           appConfig.MDM.MacOSSetup,
	}
	controlsRaw, err := cmd.generateControls(nil, "", &mdmConfig)
	require.NoError(t, err)
	require.NotNil(t, controlsRaw)
	var controls map[string]interface{}
	b, err := yaml.Marshal(controlsRaw)
	require.NoError(t, err)
	fmt.Println("Controls raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &controls)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalControls.yaml")
	require.NoError(t, err)
	var expectedControls map[string]interface{}
	err = yaml.Unmarshal(b, &expectedControls)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedControls, controls)

	if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-macos-mobileconfig-profile.mobileconfig"]; ok {
		require.Equal(t, "<xml>global macos mobileconfig profile</xml>", fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-macos-json-profile.json"]; ok {
		require.Equal(t, `{"profile": "global macos json profile"}`, fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-windows-profile.xml"]; ok {
		require.Equal(t, "<xml>global windows profile</xml>", fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-android-profile.json"]; ok {
		require.Equal(t, `{"name": "Global Android Profile", "cameraDisabled": true}`, fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	// Generate controls for no-team, sending in an MDM config with "EndUserAuthentication" disabled.
	// Mocks for no team don't return any scripts, bootstrap, software, or profiles,
	// so it should _not_ generate a macos_setup config.
	mdmConfig = fleet.TeamMDM{
		MacOSSetup: fleet.MacOSSetup{
			EnableEndUserAuthentication: false,
		},
	}
	controlsRaw, err = cmd.generateControls(ptr.Uint(0), "no_team", &mdmConfig)
	require.NoError(t, err)
	// Check that the controls do not contain a macos_setup section
	_, ok := controlsRaw["macos_setup"]
	require.False(t, ok, "Expected no macos_setup section for no-team controls")

	// Try that again, but with an MDM config that has "EndUserAuthentication" enabled.
	mdmConfig = fleet.TeamMDM{
		MacOSSetup: fleet.MacOSSetup{
			EnableEndUserAuthentication: true,
		},
	}
	controlsRaw, err = cmd.generateControls(ptr.Uint(0), "no_team", &mdmConfig)
	require.NoError(t, err)
	// Check that the controls do contain a macos_setup section
	verifyControlsHasMacosSetup(t, controlsRaw)

	// Generate controls for a team.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	// Note that this team has setup experience software, so we expect a macos_setup section.
	controlsRaw, err = cmd.generateControls(ptr.Uint(1), "some_team", nil)
	require.NoError(t, err)
	require.NotNil(t, controlsRaw)
	b, err = yaml.Marshal(controlsRaw)
	require.NoError(t, err)
	fmt.Println("Controls raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &controls)
	require.NoError(t, err)

	// Get the expected controls YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedTeamControls.yaml")
	require.NoError(t, err)
	err = yaml.Unmarshal(b, &expectedControls)
	require.NoError(t, err)

	if fileContents, ok := cmd.FilesToWrite["lib/some_team/profiles/team-macos-mobileconfig-profile.mobileconfig"]; ok {
		require.Equal(t, "<xml>test mobileconfig profile</xml>", fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	if fileContents, ok := cmd.FilesToWrite["lib/some_team/scripts/Script B.ps1"]; ok {
		require.Equal(t, "pop goes the weasel!", fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	// Compare.
	require.Equal(t, expectedControls, controls)

	// Generate controls for a team with software but none that installs during setup.
	controlsRaw, err = cmd.generateControls(ptr.Uint(2), "some_team", nil)
	require.NoError(t, err)
	require.NotNil(t, controlsRaw)
	require.False(t, ok, "Expected no macos_setup section for no-team controls")

	// Generate controls for a team with a bootstrap pacakge.
	controlsRaw, err = cmd.generateControls(ptr.Uint(3), "some_team", nil)
	require.NoError(t, err)
	verifyControlsHasMacosSetup(t, controlsRaw)

	// Generate controls for a team with a setup experience script.
	controlsRaw, err = cmd.generateControls(ptr.Uint(4), "some_team", nil)
	require.NoError(t, err)
	verifyControlsHasMacosSetup(t, controlsRaw)

	// Generate controls for a team with a setup experience profile.
	controlsRaw, err = cmd.generateControls(ptr.Uint(5), "some_team", nil)
	require.NoError(t, err)
	verifyControlsHasMacosSetup(t, controlsRaw)
}

func TestGenerateSoftware(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(cli.NewApp(), nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
		SoftwareList: make(map[uint]Software),
	}

	softwareRaw, err := cmd.generateSoftware("team.yml", 1, "some-team", false)
	require.NoError(t, err)
	require.NotNil(t, softwareRaw)
	var software map[string]interface{}
	b, err := yaml.Marshal(softwareRaw)
	require.NoError(t, err)
	fmt.Println("software raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &software)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSoftware.yaml")
	require.NoError(t, err)
	var expectedSoftware map[string]interface{}
	err = yaml.Unmarshal(b, &expectedSoftware)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedSoftware, software)

	if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-install"]; ok {
		require.Equal(t, "foo", fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-postinstall"]; ok {
		require.Equal(t, "bar", fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-uninstall"]; ok {
		require.Equal(t, "baz", fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}

	if fileContents, ok := cmd.FilesToWrite["lib/some-team/queries/my-software-package-darwin-preinstallquery.yml"]; ok {
		require.Equal(t, []map[string]interface{}{{
			"query": "SELECT * FROM pre_install_query",
		}}, fileContents)
	} else {
		t.Fatalf("Expected file not found")
	}
}

// TestGenerateSoftwareScriptPackages tests that script packages (.sh and .ps1)
// do not output install_script, post_install_script, uninstall_script, or
// pre_install_query fields in GitOps YAML, even though these fields may be
// populated internally (the file contents become the install script).
func TestGenerateSoftwareScriptPackages(t *testing.T) {
	fleetClient := &MockClientWithScriptPackage{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(cli.NewApp(), nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
		SoftwareList: make(map[uint]Software),
	}

	softwareRaw, err := cmd.generateSoftware("team.yml", 2, "script-team", false)
	require.NoError(t, err)
	require.NotNil(t, softwareRaw)

	var software map[string]interface{}
	b, err := yaml.Marshal(softwareRaw)
	require.NoError(t, err)
	fmt.Println("software with script packages:\n", string(b))

	err = yaml.Unmarshal(b, &software)
	require.NoError(t, err)

	packages, ok := software["packages"].([]interface{})
	require.True(t, ok, "packages should be an array")
	require.Len(t, packages, 3, "should have 3 packages: 1 regular + 2 scripts (.sh and .ps1)")

	// Identify by URL since hash_sha256 includes comment tokens
	var shScriptPkg, ps1ScriptPkg, regularPkg map[string]interface{}
	for _, pkg := range packages {
		p := pkg.(map[string]interface{})
		url, ok := p["url"].(string)
		if !ok {
			continue
		}
		switch url {
		case "https://example.com/download/my-script.sh":
			shScriptPkg = p
		case "https://example.com/download/setup.ps1":
			ps1ScriptPkg = p
		case "https://example.com/download/regular-package.deb":
			regularPkg = p
		}
	}

	require.NotNil(t, shScriptPkg, ".sh script package should exist")
	require.NotNil(t, ps1ScriptPkg, ".ps1 script package should exist")
	require.NotNil(t, regularPkg, "regular package should exist")

	_, hasInstallScript := shScriptPkg["install_script"]
	require.False(t, hasInstallScript, ".sh script package should NOT have install_script in YAML output")

	_, hasPostInstallScript := shScriptPkg["post_install_script"]
	require.False(t, hasPostInstallScript, ".sh script package should NOT have post_install_script in YAML output")

	_, hasUninstallScript := shScriptPkg["uninstall_script"]
	require.False(t, hasUninstallScript, ".sh script package should NOT have uninstall_script in YAML output")

	_, hasPreInstallQuery := shScriptPkg["pre_install_query"]
	require.False(t, hasPreInstallQuery, ".sh script package should NOT have pre_install_query in YAML output")

	_, hasInstallScript = ps1ScriptPkg["install_script"]
	require.False(t, hasInstallScript, ".ps1 script package should NOT have install_script in YAML output")

	_, hasPostInstallScript = ps1ScriptPkg["post_install_script"]
	require.False(t, hasPostInstallScript, ".ps1 script package should NOT have post_install_script in YAML output")

	_, hasUninstallScript = ps1ScriptPkg["uninstall_script"]
	require.False(t, hasUninstallScript, ".ps1 script package should NOT have uninstall_script in YAML output")

	_, hasPreInstallQuery = ps1ScriptPkg["pre_install_query"]
	require.False(t, hasPreInstallQuery, ".ps1 script package should NOT have pre_install_query in YAML output")

	require.Contains(t, shScriptPkg, "url", ".sh script package should have url")
	require.Contains(t, shScriptPkg, "hash_sha256", ".sh script package should have hash_sha256")
	require.Contains(t, ps1ScriptPkg, "url", ".ps1 script package should have url")
	require.Contains(t, ps1ScriptPkg, "hash_sha256", ".ps1 script package should have hash_sha256")

	require.Contains(t, regularPkg, "install_script", "regular package should have install_script")
	require.Contains(t, regularPkg, "post_install_script", "regular package should have post_install_script")
	require.Contains(t, regularPkg, "uninstall_script", "regular package should have uninstall_script")
	require.Contains(t, regularPkg, "pre_install_query", "regular package should have pre_install_query")

	for filename := range cmd.FilesToWrite {
		require.NotContains(t, filename, "my-script-linux-install", "should not write install script file for .sh script package")
		require.NotContains(t, filename, "my-script-linux-postinstall", "should not write post-install script file for .sh script package")
		require.NotContains(t, filename, "my-script-linux-uninstall", "should not write uninstall script file for .sh script package")
		require.NotContains(t, filename, "my-script-linux-preinstallquery", "should not write pre-install query file for .sh script package")

		require.NotContains(t, filename, "powershell-script-windows-install", "should not write install script file for .ps1 script package")
		require.NotContains(t, filename, "powershell-script-windows-postinstall", "should not write post-install script file for .ps1 script package")
		require.NotContains(t, filename, "powershell-script-windows-uninstall", "should not write uninstall script file for .ps1 script package")
		require.NotContains(t, filename, "powershell-script-windows-preinstallquery", "should not write pre-install query file for .ps1 script package")
	}
}

type MockClientWithScriptPackage struct {
	MockClient
}

func (c *MockClientWithScriptPackage) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResult, error) {
	switch query {
	case "available_for_install=1&team_id=2":
		return []fleet.SoftwareTitleListResult{
			{
				ID:         3,
				Name:       "My Script",
				HashSHA256: ptr.String("script-package-hash"),
				SoftwarePackage: &fleet.SoftwarePackageOrApp{
					Name:     "my-script.sh",
					Platform: "linux",
					Version:  "1.0",
				},
			},
			{
				ID:         4,
				Name:       "Regular Package",
				HashSHA256: ptr.String("regular-package-hash"),
				SoftwarePackage: &fleet.SoftwarePackageOrApp{
					Name:     "regular-package.deb",
					Platform: "linux",
					Version:  "2.0",
				},
			},
			{
				ID:         5,
				Name:       "PowerShell Script",
				HashSHA256: ptr.String("ps1-script-hash"),
				SoftwarePackage: &fleet.SoftwarePackageOrApp{
					Name:     "setup.ps1",
					Platform: "windows",
					Version:  "1.5",
				},
			},
		}, nil
	default:
		return c.MockClient.ListSoftwareTitles(query)
	}
}

func (c *MockClientWithScriptPackage) GetSoftwareTitleByID(id uint, teamID *uint) (*fleet.SoftwareTitle, error) {
	switch id {
	case 3:
		if *teamID != 2 {
			return nil, errors.New("team ID mismatch")
		}
		// InstallScript is populated internally from file contents, but these fields
		// should NOT be output in GitOps YAML
		return &fleet.SoftwareTitle{
			ID: 3,
			SoftwarePackage: &fleet.SoftwareInstaller{
				InstallScript:     "#!/bin/bash\necho 'This is the script content'",
				PostInstallScript: "",
				UninstallScript:   "",
				PreInstallQuery:   "",
				SelfService:       true,
				Platform:          "linux",
				URL:               "https://example.com/download/my-script.sh",
				Name:              "my-script.sh",
			},
		}, nil
	case 4:
		if *teamID != 2 {
			return nil, errors.New("team ID mismatch")
		}
		return &fleet.SoftwareTitle{
			ID: 4,
			SoftwarePackage: &fleet.SoftwareInstaller{
				InstallScript:     "install script content",
				PostInstallScript: "post-install script content",
				UninstallScript:   "uninstall script content",
				PreInstallQuery:   "SELECT 1",
				SelfService:       false,
				Platform:          "linux",
				URL:               "https://example.com/download/regular-package.deb",
				Name:              "regular-package.deb",
			},
		}, nil
	case 5:
		if *teamID != 2 {
			return nil, errors.New("team ID mismatch")
		}
		// InstallScript is populated internally from file contents, but these fields
		// should NOT be output in GitOps YAML
		return &fleet.SoftwareTitle{
			ID: 5,
			SoftwarePackage: &fleet.SoftwareInstaller{
				InstallScript:     "Write-Host 'This is the PowerShell script content'",
				PostInstallScript: "",
				UninstallScript:   "",
				PreInstallQuery:   "",
				SelfService:       true,
				Platform:          "windows",
				URL:               "https://example.com/download/setup.ps1",
				Name:              "setup.ps1",
			},
		}, nil
	default:
		return c.MockClient.GetSoftwareTitleByID(id, teamID)
	}
}

func (c *MockClientWithScriptPackage) GetSetupExperienceSoftware(platform string, teamID uint) ([]fleet.SoftwareTitleListResult, error) {
	if teamID == 2 {
		return []fleet.SoftwareTitleListResult{}, nil
	}
	return c.MockClient.GetSetupExperienceSoftware(platform, teamID)
}

func TestGeneratePolicies(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(cli.NewApp(), nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
		SoftwareList: map[uint]Software{
			1: {
				Hash:    "team-software-hash",
				Comment: "__TEAM_SOFTWARE_COMMENT_TOKEN__",
			},
		},
		ScriptList: map[uint]string{
			1: "/path/to/script1.sh",
		},
	}

	policiesRaw, err := cmd.generatePolicies(nil, "default.yml")
	require.NoError(t, err)
	require.NotNil(t, policiesRaw)
	var policies []map[string]interface{}
	b, err := yaml.Marshal(policiesRaw)
	require.NoError(t, err)
	fmt.Println("policies raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &policies)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalPolicies.yaml")
	require.NoError(t, err)
	var expectedPolicies []map[string]interface{}
	err = yaml.Unmarshal(b, &expectedPolicies)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedPolicies, policies)

	// Generate policies for a team.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	policiesRaw, err = cmd.generatePolicies(ptr.Uint(1), "some_team")
	require.NoError(t, err)
	require.NotNil(t, policiesRaw)
	b, err = yaml.Marshal(policiesRaw)
	require.NoError(t, err)
	fmt.Println("policies raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &policies)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedTeamPolicies.yaml")
	require.NoError(t, err)
	err = yaml.Unmarshal(b, &expectedPolicies)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedPolicies, policies)
}

func TestGenerateQueries(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(cli.NewApp(), nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
	}

	queriesRaw, err := cmd.generateQueries(nil)
	require.NoError(t, err)
	require.NotNil(t, queriesRaw)
	var queries []map[string]interface{}
	b, err := yaml.Marshal(queriesRaw)
	require.NoError(t, err)
	fmt.Println("queries raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &queries)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalQueries.yaml")
	require.NoError(t, err)
	var expectedQueries []map[string]interface{}
	err = yaml.Unmarshal(b, &expectedQueries)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedQueries, queries)

	// Generate queries for a team.
	// Note that nested keys here may be strings,
	// so we'll JSON marshal and unmarshal to a map for comparison.
	queriesRaw, err = cmd.generateQueries(ptr.Uint(1))
	require.NoError(t, err)
	require.NotNil(t, queriesRaw)
	b, err = yaml.Marshal(queriesRaw)
	require.NoError(t, err)
	fmt.Println("queries raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &queries)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedTeamQueries.yaml")
	require.NoError(t, err)
	err = yaml.Unmarshal(b, &expectedQueries)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedQueries, queries)
}

func TestGenerateLabels(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(cli.NewApp(), nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
	}

	labelsRaw, err := cmd.generateLabels()
	require.NoError(t, err)
	require.NotNil(t, labelsRaw)
	var labels []map[string]interface{}
	b, err := yaml.Marshal(labelsRaw)
	require.NoError(t, err)
	fmt.Println("labels raw:\n", string(b)) // Debugging line
	err = yaml.Unmarshal(b, &labels)
	require.NoError(t, err)

	// Get the expected org settings YAML.
	b, err = os.ReadFile("./testdata/generateGitops/expectedLabels.yaml")
	require.NoError(t, err)
	var expectedlabels []map[string]interface{}
	err = yaml.Unmarshal(b, &expectedlabels)
	require.NoError(t, err)

	// Compare.
	require.Equal(t, expectedlabels, labels)
}

func verifyControlsHasMacosSetup(t *testing.T, controlsRaw map[string]interface{}) {
	macosSetup, ok := controlsRaw["macos_setup"].(string)
	require.True(t, ok, "Expected macos_setup section to be a string")
	require.Equal(t, macosSetup, "TODO: update with your macos_setup configuration")
}

func TestGenerateControlsAndMDMWithoutMDMEnabledAndConfigured(t *testing.T) {
	// Get the test app config.
	fleetClient := &MockClient{}
	appConfig, err := fleetClient.GetAppConfig()
	require.NoError(t, err)
	appConfig.MDM.EnabledAndConfigured = false
	appConfig.MDM.WindowsEnabledAndConfigured = false
	appConfig.MDM.AppleBusinessManager = optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{}
	appConfig.MDM.AppleServerURL = ""
	appConfig.MDM.EndUserAuthentication = fleet.MDMEndUserAuthentication{}
	appConfig.MDM.VolumePurchasingProgram = optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{}

	// Create the command.
	cmd := &GenerateGitopsCommand{
		Client:       fleetClient,
		CLI:          cli.NewContext(cli.NewApp(), nil, nil),
		Messages:     Messages{},
		FilesToWrite: make(map[string]interface{}),
		AppConfig:    appConfig,
		ScriptList:   make(map[uint]string),
	}
	// teamId=6 is unhandled for MDM APIs to make sure the APIs to get MDM stuff
	// aren't called if MDM is not enabled and configured.
	controlsRaw, err := cmd.generateControls(ptr.Uint(6), "some_team", nil)
	require.NoError(t, err)
	require.Contains(t, controlsRaw, "scripts") // Uploading scripts does not require MDM turned on.
	require.Empty(t, controlsRaw["scripts"])

	// teamId=6 is unhandled for MDM APIs to make sure the APIs to get MDM stuff
	// aren't called if MDM is not enabled and configured.
	mdmRaw, err := cmd.generateMDM(&appConfig.MDM)
	require.NoError(t, err)
	// Verify all keys are set to empty.
	for _, key := range []string{
		"apple_business_manager",
		"apple_server_url",
		"end_user_authentication",
		"end_user_license_agreement",
		"volume_purchasing_program",
	} {
		require.Contains(t, mdmRaw, key)
		require.Empty(t, mdmRaw[key])
	}
}

func TestSillyTeamNames(t *testing.T) {
	sillyTeamNames := map[string]string{
		"🫆": "🫆.yml",
		"🪾": "🪾.yml",
		"🫜": "🫜.yml",
		"🪉": "🪉.yml",
		"🪏": "🪏.yml",
		"🫟": "🫟.yml",
		"👍": "👍.yml",
		"a/team\\with:all*the?footguns\"in<it>omg|": "aateambwithcalldtheefootgunsfingithomgi.yml",
	}

	fleetClient := &MockClient{}
	tempDir := os.TempDir() + "/" + uuid.New().String()

	t.Cleanup(func() {
		if err := os.RemoveAll(tempDir); err != nil {
			t.Fatalf("failed to remove temp dir: %v", err)
		}
	})

	for name, expectedFilename := range sillyTeamNames {
		t.Run(name, func(t *testing.T) {
			flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
			flagSet.String("dir", tempDir, "")
			flagSet.Bool("force", true, "")
			fleetClient.TeamNameOverride = name
			action := createGenerateGitopsAction(fleetClient)
			buf := new(bytes.Buffer)
			cliContext := cli.NewContext(&cli.App{
				Name:      "test",
				Usage:     "test",
				Writer:    buf,
				ErrWriter: buf,
			}, flagSet, nil)
			// Get the test app config.
			err := action(cliContext)
			require.NoError(t, err, buf.String())

			// Expect a correctly-named .yaml
			tgtPath := filepath.Join(tempDir, "teams", expectedFilename)
			_, err = os.Stat(tgtPath)
			require.NoError(t, err)
		})
	}
}
